Cache, invalidation and reload

INTRODUCTION

diginsight SmartCache provides hybrid, distributed, multilevel caching based on age sensitive data management.

This article discusses how we can use SmartCache to:

  • Cache data from a call and associate a suitable key to it
  • Invalidate entries
  • Reload cache entries upon invalidation

The code snippets and images below are taken from the SampleWebAPI working samples within the smartcache.samples repository.

STEP 01 - Cache data from a call and associate a suitable key to it

Let’s assume we need to cache data from a long latency operation such as:

public async Task<Plant> GetPlantByIdImplAsync([FromRoute] Guid id)
{
    using var activity = Program.ActivitySource.StartMethodActivity(logger, new { id });

    var result = default(IEnumerable<Plant>);

    // ... implementation ...

    activity?.SetOutput(plant);
    return plant;
}

The result from the method executions can be cached with the following steps:

  • inject smartCache and cacheKeyService into the current class
    alt text

  • create a new method GetPlantByIdAsync calling GetPlantByIdImplAsync by means of smartCache service

    public async Task<Plant> GetPlantByIdAsync([FromRoute] Guid plantId)
    {
        using var activity = Program.ActivitySource.StartMethodActivity(logger);
    
        var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) };
        var cacheKey = new MethodCallCacheKey(cacheKeyService, typeof(PlantsController), nameof(GetPlantByIdAsync), plantId);
    
        var plant = await smartCache.GetAsync(cacheKey, _ => GetPlantByIdImplAsync(plantId), options);
    
        activity?.SetOutput(plant);
        return plant;
    }

    the smartCache.GetAsyc() call manages cached call to GetPlantByIdImplAsync:

    • cacheKey: is the cache key class associated to the cache entry
    • delegate with the call to GetPlantByIdImplAsync is used to fetch values in case of cache miss
    • (opt) options with required MaxAge allows requesting data specifying a specific age criteria: cache hit will happen only if available data is within the requested age.

calling GetPlantByIdAsync twice, you will get the following log where:

  • the first call is a cache miss with latency of >1sec
  • the first call is a cache hit with latency of few ms

alt text

STEP 02 - Add Invalidation support to cached calls

Assume you are calling a cached method with the following code

public async Task<IEnumerable<Plant>> GetPlantsAsync()
{
    using var activity = Program.ActivitySource.StartMethodActivity(logger);

    var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) };
    var cacheKey = new GetPlantByIdCacheKey(Guid.Empty);

    Task<IEnumerable<Plant>> getCachedValuesAsync() =>
        smartCache.GetAsync(cacheKey, _ => GetPlantsImplAsync(), options);

    var plants = await getCachedValuesAsync();
    activity?.SetOutput(plants);
    return plants;
}

You can add support to invalidation deriving your key from IInvalidatable:

internal sealed record GetPlantByIdCacheKey(Guid PlantId) : IInvalidatable
{
    public bool IsInvalidatedBy(IInvalidationRule invalidationRule, out Func<Task> ic)
    {
        ic = null;
        if (invalidationRule is PlantInvalidationRule pir && (PlantId == Guid.Empty || pir.PlantId == PlantId))
        {
            return true;
        }
        return false;
    }
}

Upon plant creation/update/delete, you can trigger invalidation by means of smartCache.Invalidate(); call:

alt text

When Invalidating a plantId, all keys will be enumerated and those invalidated by the ID will be dismissed by the cache.

Calling GetPlantsAsync after an update to the Plant, you will get a cache miss as the entry associated to the call has been dismissed.

The image below shows that: - after updating a Plant - the GetPlantsAsync call gets a cache miss call as its cache entry has been invalidated

alt text

STEP 03 - Add automatic reload support to cached calls

Assume you are calling a cached method with the following code

public async Task<IEnumerable<Plant>> GetPlantsAsync()
{
    using var activity = Program.ActivitySource.StartMethodActivity(logger);

    var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) };
    var cacheKey = new GetPlantByIdCacheKey(cacheKeyService, Guid.Empty);

    Task<IEnumerable<Plant>> getCachedValuesAsync() =>
        smartCache.GetAsync(cacheKey, _ => GetPlantsImplAsync(), options);
    cacheKey.ReloadAsync = getCachedValuesAsync; 

    var plants = await getCachedValuesAsync();
    activity?.SetOutput(plants);
    return plants;
}

In this case, getCachedValuesAsync delegate is used to load data.
Also, getCachedValuesAsync is assigned to cacheKey.ReloadAsync property to enable cache entry reload, after invalidation.

When Invalidating a plantId, all keys will be enumerated and those invalidated by the ID will be dismissed by the cache.
If ReloadAsync delegate is available, after invalidation, the delegate is invoked to load the cache entry again.

Calling GetPlantsAsync after an update to the Plant, this time you will get a cache hit as the entry associated to the call has been reloaded after invalidation.

The image below shows that:

  • after updating a Plant
  • the GetPlantsAsync call gets a cache hit call as its cache entry has been reloaded, after invalidation

alt text
Back to top